Skip to content

feat(claude-code): emit installationPreference in marketplace.json entries - #41430

Open
devin-ai-integration[bot] wants to merge 14 commits into
mainfrom
litellm_marketplace_installation_preference
Open

devin-ai-integration[bot] wants to merge 14 commits into
mainfrom
litellm_marketplace_installation_preference

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • marketplace.json entries never carry installationPreference
  • Claude Desktop treats every same-origin plugin as manual install only

How it solves it:

  • PluginSpec gains an optional installation_preference field
  • Only available, auto_install or required are accepted, anything else is a 422
  • marketplace.json emits it as installationPreference per plugin

User Flow

Before: a proxy admin registers an internal plugin and Claude Desktop will not auto-install it

  1. They send POST https://litellm-domain/claude-code/plugins with {"name": "my-plugin", "source": {"source": "archive", "url": "https://files.example.com/my-plugin.zip", "sha256": "..."}} and get back 200 with "action": "created"
  2. They open https://litellm-domain/claude-code/marketplace.json and the entry has name, source, version but nothing about installation
  3. Claude Desktop, pointed at the same origin, lists the plugin as manual install only

After: the same admin marks the plugin for auto install and Claude Desktop picks it up

  1. They send POST https://litellm-domain/claude-code/plugins with the same body plus "installation_preference": "auto_install" and get back 200 with "action": "created"
  2. They open https://litellm-domain/claude-code/marketplace.json and the entry now carries "installationPreference": "auto_install"
  3. Claude Desktop, pointed at the same origin, auto-installs the plugin

A value outside available, auto_install or required is rejected with 422 rather than silently dropped. The same field is accepted by PUT https://litellm-domain/claude-code/plugins/{name} and echoed back as installation_preference by GET https://litellm-domain/claude-code/plugins and GET https://litellm-domain/claude-code/plugins/{name}. Entries without it are unchanged

Relevant issues

Feature request from a customer (Pylon #8704)

Linear ticket

Resolves LIT-7868

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Last updated: 442ed5a, which adds installationPreference to the exported MarketplacePluginEntry model so a client that validates the catalog as MarketplaceResponse keeps the field, with a round-trip and an invalid-value unit test. The route still returns its JSON directly, so the served marketplace.json and the OpenAPI snapshot are unchanged. The commit before it, 1c16857, merges main in and carries two lint-only fixes: the marketplace helper is public as get_installation_preference and the two call sites that read a stored manifest pass it through a TypeAdapter boundary first, and the audit's worker-kill cell uses the harness's kill_worker. None of these changes the served behavior, so QA and /live-pr-risk carry from 19a6c00, the last commit with a product diff, and the /audit file rerun below is the live proof of the refactored lines

Two proxies booted from this rig, each with --num_workers 2 on a random port and its own fresh Postgres database: the before leg at the merge base 118ce3c on port 31361 and the after leg at this PR's tip 19a6c00 on port 50017. The proxy admin's calls are the curl commands below, run as typed with the leg's master key in $LITELLM_MASTER_KEY. The end-user client is Claude Code v2.1.286 driven interactively under tmux against each proxy's marketplace.json. Claude Desktop is the client that acts on installationPreference, and only under managed configuration, so it was not driven here; what it reads is the marketplace.json shown in each leg. Docs: BerriAI/litellm-docs#1497

Before (118ce3c)

Register one plugin with installation_preference and one without, read every surface that serves them back, then send a value outside the allowed set

$ curl -s -X POST http://127.0.0.1:31361/claude-code/plugins -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d '{"name":"internal-auto-plugin","version":"1.0.0","description":"Auto install demo","source":{"source":"archive","url":"https://files.example.com/internal-auto-plugin.zip","sha256":"'$(printf 'a%.0s' {1..64})'"},"installation_preference":"auto_install"}'
{"status":"success","action":"created","plugin":{"id":"0c6549d4-87b2-4e9d-903a-1c0432929b11","name":"internal-auto-plugin","version":"1.0.0","description":"Auto install demo","source":{"source":"archive","url":"https://files.example.com/internal-auto-plugin.zip","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},"enabled":true}}
$ curl -s -X POST http://127.0.0.1:31361/claude-code/plugins -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d '{"name":"internal-manual-plugin","version":"1.0.0","description":"Manual install demo","source":{"source":"archive","url":"https://files.example.com/internal-manual-plugin.zip","sha256":"'$(printf 'b%.0s' {1..64})'"}}' | jq -c '{status, action, name: .plugin.name}'
{"status":"success","action":"created","name":"internal-manual-plugin"}
$ curl -s http://127.0.0.1:31361/claude-code/marketplace.json | jq -c '.plugins[]'
{"name":"internal-auto-plugin","source":{"source":"archive","url":"https://files.example.com/internal-auto-plugin.zip","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},"version":"1.0.0","description":"Auto install demo"}
{"name":"internal-manual-plugin","source":{"source":"archive","url":"https://files.example.com/internal-manual-plugin.zip","sha256":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"},"version":"1.0.0","description":"Manual install demo"}
$ curl -s http://127.0.0.1:31361/claude-code/plugins/internal-auto-plugin -H "Authorization: Bearer $LITELLM_MASTER_KEY" | jq -c 'with_entries(select(.key == "name" or .key == "installation_preference"))'
{"name":"internal-auto-plugin"}
$ curl -s http://127.0.0.1:31361/claude-code/plugins -H "Authorization: Bearer $LITELLM_MASTER_KEY" | jq -c '.plugins[] | with_entries(select(.key == "name" or .key == "installation_preference"))'
{"name":"internal-manual-plugin"}
{"name":"internal-auto-plugin"}
$ curl -s http://127.0.0.1:31361/public/skill_hub | jq -c '.plugins[] | with_entries(select(.key == "name" or .key == "installation_preference"))'
{"name":"internal-auto-plugin"}
{"name":"internal-manual-plugin"}
$ curl -s -w '\nHTTP %{http_code}\n' -X POST http://127.0.0.1:31361/claude-code/plugins -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d '{"name":"bad-plugin","version":"1.0.0","description":"Bad value demo","source":{"source":"archive","url":"https://files.example.com/bad-plugin.zip","sha256":"'$(printf 'c%.0s' {1..64})'"},"installation_preference":"sometimes"}'
{"status":"success","action":"created","plugin":{"id":"514ad4fd-12c3-4dfd-a6f7-358800e33492","name":"bad-plugin","version":"1.0.0","description":"Bad value demo","source":{"source":"archive","url":"https://files.example.com/bad-plugin.zip","sha256":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"},"enabled":true}}
HTTP 200
$ curl -s http://127.0.0.1:31361/claude-code/marketplace.json | jq -c '[.plugins[].name]'
["internal-auto-plugin","internal-manual-plugin","bad-plugin"]

Claude Code v2.1.286 under tmux: /plugin marketplace add http://127.0.0.1:31361/claude-code/marketplace.json, then /plugins, internal typed into the Discover search, Enter on internal-auto-plugin

pr41430-19a6c00ed8-before-cli-plugins.png

pr41430-19a6c00ed8-before-cli-plugin-detail.png

Replace the plugin with a body that carries only source

$ curl -s -X PUT http://127.0.0.1:31361/claude-code/plugins/internal-auto-plugin -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d '{"source":{"source":"archive","url":"https://files.example.com/internal-auto-plugin.zip","sha256":"'$(printf 'a%.0s' {1..64})'"}}' | jq -c '{status, action, name: .plugin.name}'
{"status":"success","action":"updated","name":"internal-auto-plugin"}
$ curl -s http://127.0.0.1:31361/claude-code/marketplace.json | jq -c '.plugins[] | select(.name == "internal-auto-plugin")'
{"name":"internal-auto-plugin","source":{"source":"archive","url":"https://files.example.com/internal-auto-plugin.zip","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}
$ curl -s http://127.0.0.1:31361/claude-code/plugins/internal-auto-plugin -H "Authorization: Bearer $LITELLM_MASTER_KEY" | jq -c 'with_entries(select(.key == "name" or .key == "installation_preference"))'
{"name":"internal-auto-plugin"}

After (19a6c00)

Register one plugin with installation_preference and one without, read every surface that serves them back, then send a value outside the allowed set

$ curl -s -X POST http://127.0.0.1:50017/claude-code/plugins -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d '{"name":"internal-auto-plugin","version":"1.0.0","description":"Auto install demo","source":{"source":"archive","url":"https://files.example.com/internal-auto-plugin.zip","sha256":"'$(printf 'a%.0s' {1..64})'"},"installation_preference":"auto_install"}'
{"status":"success","action":"created","plugin":{"id":"6825ec14-0ac9-4284-859f-51add37e299c","name":"internal-auto-plugin","version":"1.0.0","description":"Auto install demo","source":{"source":"archive","url":"https://files.example.com/internal-auto-plugin.zip","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},"enabled":true}}
$ curl -s -X POST http://127.0.0.1:50017/claude-code/plugins -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d '{"name":"internal-manual-plugin","version":"1.0.0","description":"Manual install demo","source":{"source":"archive","url":"https://files.example.com/internal-manual-plugin.zip","sha256":"'$(printf 'b%.0s' {1..64})'"}}' | jq -c '{status, action, name: .plugin.name}'
{"status":"success","action":"created","name":"internal-manual-plugin"}
$ curl -s http://127.0.0.1:50017/claude-code/marketplace.json | jq -c '.plugins[]'
{"name":"internal-auto-plugin","source":{"source":"archive","url":"https://files.example.com/internal-auto-plugin.zip","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},"installationPreference":"auto_install","version":"1.0.0","description":"Auto install demo"}
{"name":"internal-manual-plugin","source":{"source":"archive","url":"https://files.example.com/internal-manual-plugin.zip","sha256":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"},"version":"1.0.0","description":"Manual install demo"}
$ curl -s http://127.0.0.1:50017/claude-code/plugins/internal-auto-plugin -H "Authorization: Bearer $LITELLM_MASTER_KEY" | jq -c 'with_entries(select(.key == "name" or .key == "installation_preference"))'
{"name":"internal-auto-plugin","installation_preference":"auto_install"}
$ curl -s http://127.0.0.1:50017/claude-code/plugins -H "Authorization: Bearer $LITELLM_MASTER_KEY" | jq -c '.plugins[] | with_entries(select(.key == "name" or .key == "installation_preference"))'
{"name":"internal-manual-plugin","installation_preference":null}
{"name":"internal-auto-plugin","installation_preference":"auto_install"}
$ curl -s http://127.0.0.1:50017/public/skill_hub | jq -c '.plugins[] | with_entries(select(.key == "name" or .key == "installation_preference"))'
{"name":"internal-auto-plugin","installation_preference":"auto_install"}
{"name":"internal-manual-plugin","installation_preference":null}
$ curl -s -w '\nHTTP %{http_code}\n' -X POST http://127.0.0.1:50017/claude-code/plugins -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d '{"name":"bad-plugin","version":"1.0.0","description":"Bad value demo","source":{"source":"archive","url":"https://files.example.com/bad-plugin.zip","sha256":"'$(printf 'c%.0s' {1..64})'"},"installation_preference":"sometimes"}'
{"detail":[{"type":"literal_error","loc":["body","installation_preference"],"msg":"Input should be 'available', 'auto_install' or 'required'"}]}
HTTP 422
$ curl -s http://127.0.0.1:50017/claude-code/marketplace.json | jq -c '[.plugins[].name]'
["internal-auto-plugin","internal-manual-plugin"]

Claude Code v2.1.286 under tmux: /plugin marketplace add http://127.0.0.1:50017/claude-code/marketplace.json, then /plugins, internal typed into the Discover search, Enter on internal-auto-plugin

pr41430-19a6c00ed8-after-cli-plugins.png

pr41430-19a6c00ed8-after-cli-plugin-detail.png

Replace the plugin with a body that carries only source

$ curl -s -X PUT http://127.0.0.1:50017/claude-code/plugins/internal-auto-plugin -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d '{"source":{"source":"archive","url":"https://files.example.com/internal-auto-plugin.zip","sha256":"'$(printf 'a%.0s' {1..64})'"}}' | jq -c '{status, action, name: .plugin.name}'
{"status":"success","action":"updated","name":"internal-auto-plugin"}
$ curl -s http://127.0.0.1:50017/claude-code/marketplace.json | jq -c '.plugins[] | select(.name == "internal-auto-plugin")'
{"name":"internal-auto-plugin","source":{"source":"archive","url":"https://files.example.com/internal-auto-plugin.zip","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}
$ curl -s http://127.0.0.1:50017/claude-code/plugins/internal-auto-plugin -H "Authorization: Bearer $LITELLM_MASTER_KEY" | jq -c 'with_entries(select(.key == "name" or .key == "installation_preference"))'
{"name":"internal-auto-plugin","installation_preference":null}

Dependents driven live (/live-pr-risk)

The bundled Admin UI, logged in as the admin on each leg, at /ui/skills and the internal-auto-plugin detail page. Both legs render the same rows and fields; the page reads name, version, description, source and id, never the preference. The before leg lists bad-plugin because that proxy accepted the invalid value

pr41430-19a6c00ed8-before-ui-skills.png

pr41430-19a6c00ed8-before-ui-plugin-detail.png

pr41430-19a6c00ed8-after-ui-skills.png

pr41430-19a6c00ed8-after-ui-plugin-detail.png

Merge-ref check: this PR merged onto origin/main 6fd9334 (a throwaway merge commit, tree e1eabc8962) booted the same way on port 49052, and the whole command sequence above produced the same output as the after leg, the generated plugin id aside

Observations from the legs

  • CLI shows no preference label on either leg: leaves alone
  • POST and PUT responses omit installation_preference: leaves alone
  • PUT with only source drops version and description: leaves alone
  • Marketplace plugin order flips after PUT, both legs: leaves alone
  • Claude Code auto-runs /reload-plugins after marketplace add: leaves alone
  • Admin UI cannot set or show the preference: leaves alone

/audit: integration cells, base 118ce3c vs head 1c16857 (rows carried from 1ec6486, the file rerun green at 1c16857)

One checked-in test per inventory row in tests/integration/management/test_claude_code_plugin_installation_preference.py (group management), run against a real proxy rig: owned Redis, a fresh Postgres database per leg, the scripted upstream from tests/integration/_support/upstream.py, and per leg two proxy processes sharing that database (primary and peer) booted from the leg's commit, no mocks inside the proxy. The chaos rows boot their own two-worker proxies through owned_proxy_process. Command per run: python tests/integration/run.py management --seed 4106601 --order-seed 0 <file>

Run Commit Result
head run 1 1ec6486 36 passed, 0 skipped, exit 0
head run 2 1ec6486 35 passed; row 21's own two-worker proxy missed the 70s readiness budget (box load)
head run 3 1ec6486 35 passed; same readiness miss on row 21
head run 4 1ec6486 35 passed; same readiness miss on row 21
base run 1 118ce3c 36 failed, 0 skipped
head, whole management group 1ec6486 418 passed, 13 failed, 7 skipped in 49:48; every failure an owned proxy missing the suite's 70s readiness budget under box load, none a product assertion
head run 5 1c16857 32 passed, 4 setup errors: the rig had no peer proxy up yet (INTEGRATION_PEER_URL unset), so rows 3, 17, 19, and 20 never ran; row 21 PASS, its owned two-worker proxy ready in 178s under INTEGRATION_PROXY_READY_SECONDS=180 on a box at 35 GB of swap
head run 6 1c16857 rows 3, 17, 19, and 20 rerun with the peer proxy up: 4 passed in 17s

Every cell asserts the new field, so every cell is red on the base by construction; the base column names where the base first diverged

# Scenario Node id Base 118ce3c Head 1ec6486
1 each value served on marketplace.json test_registered_preference_is_served_on_the_marketplace[available|auto_install|required] no installationPreference PASS x4
2 list, detail, skill hub echo test_preference_echoes_on_list_detail_and_skill_hub no field PASS x4
3 written on primary, read on peer test_peer_process_serves_the_stored_preference no field PASS x4
4 POST without the field test_omitted_preference_is_unset key absent (head: null) PASS x4
5 POST with null test_explicit_null_preference_is_unset key absent (head: null) PASS x4
6 POST with six invalid shapes test_invalid_preference_is_refused_on_register[6 ids] 200, value dropped PASS x4, 422
7 PUT with six invalid shapes test_invalid_preference_is_refused_on_update[6 ids] 200, value dropped PASS x4, 422
8 raw JSON key twice test_duplicate_json_key_keeps_the_last_value no field PASS x4
9 PUT twice, same value test_repeated_identical_update_is_idempotent no field PASS x4
10 duplicate name test_duplicate_name_is_refused_and_keeps_the_first_preference 409 held, then no field PASS x4
11 PUT changes then clears test_update_changes_and_clears_the_preference no field PASS x4
12 PUT whole-object diff test_update_replaces_the_whole_manifest no field PASS x4
13 unauthenticated writes test_unauthenticated_requests_are_refused 401s held, then no field PASS x4
14 non-admin writes test_non_admin_key_cannot_modify_the_catalog 403s held, then no field PASS x4
15 disabled plugin, granted key test_disabled_plugin_keeps_its_preference_for_granted_keys no field PASS x4
16 disabled plugin, non-granted key test_non_granted_key_cannot_read_a_disabled_plugin no field at the first read PASS x4
17 hand-edited unknown value test_hand_edited_unknown_preference_is_not_served key absent (head: null) PASS x4
18 openapi.json enum test_openapi_declares_the_preference_enum[3 schemas] property missing PASS x4
19 13 concurrent PUTs under 40 reads test_burst_reads_across_processes_while_the_preference_flips every read None PASS x4
20 repeated identical reads test_repeated_identical_reads_are_stable no field PASS x4
21 worker killed mid-burst test_registration_burst_survives_a_worker_kill no field, all POSTs landed PASS at 1c16857 in run 5 (owned proxy ready in 178s against a 180s readiness budget) and at 1ec6486 in run 1; runs 2 to 4 there missed the suite's 70s budget before the first request (box load)
22 proxy restart after a burst test_registrations_survive_a_proxy_restart no field, all rows survived PASS x4
23 Claude Desktop auto-install none (live cell) not run gateway half live over an HTTPS tailnet origin at 1c16857's product code: marketplace.json served "installationPreference":"auto_install" with and without a bearer, the plugin archive answered 200 from the same origin, /v1/models listed the Claude ids, /v1/messages answered; the Claude Desktop half not run, since the installed app is the consumer build, which switches to a gateway only through an MDM-delivered managed profile

Audit verdict: PASS at 1c16857 under the /audit cap, with row 23's Claude Desktop half accepted on the Medium caveat below. Rows 1 to 22 are green at this tip across runs 5 and 6, row 21 included (its earlier misses at 1ec6486 were the suite's 70s readiness budget on a loaded box, cleared here with a 180s budget and a pass at 178s). Row 23's gateway half ran live: a LiteLLM proxy at this code behind an HTTPS tailnet origin served the marketplace with the field, the archive, and inference, which is everything Claude Desktop reads; the install itself needs a managed Claude Desktop deployment whose inferenceGatewayBaseUrl is that origin, which this rig cannot provide. Pre-existing on both legs: a non-admin key gets 401 on /claude-code/plugins/{name}/enable and /disable but 403 on POST, PUT, and DELETE, and concurrent PUTs on one plugin are last-commit-wins

Type

🆕 New Feature

Caveats (if any)

Medium

  • Claude Desktop's auto-install itself was not driven, only the contract it reads, served live over HTTPS
    • Row 23 above: the gateway half ran live (an HTTPS origin serving marketplace.json with the field, the archive, and /v1/messages), but the installed Claude Desktop is the consumer build, which switches to a gateway only through an MDM-delivered managed profile, so the install itself is unobserved and is accepted on the assumption that a marketplace served exactly as the doc specifies is honored. The field name, its three values, the archive plus sha256 rule, and the same-origin rule were checked against Anthropic's Claude Desktop extensions doc on 2026-09-30. Closing it needs a managed Claude Desktop deployment whose inferenceGatewayBaseUrl is an HTTPS LiteLLM origin that also serves the plugin archive
  • auto_install and required are accepted on any source type, while Claude Desktop honors them only on an archive source with a sha256 served from its gateway origin
    • A github or git-subdir plugin marked auto_install is served as marked and silently stays manual install in Claude Desktop. The alternative is a 422 on the mark for a non-archive source. Not done here because Anthropic's doc pins per-plugin marks to archives by example only, so it is left open for the reviewer

Low

  • unit / mcp-server went red once at 1c16857 on test_cold_concurrent_schema_validation_accepts_valid_arguments, not from this PR
    • The known flake of #46071 in a file this PR does not touch; main's runs 38332420650 and 38332162780 are green on the shard, and the one rerun from failed is the fix here, no change in this PR
  • A stored installation_preference outside the three values is not served
    • Only reachable by editing manifest_json by hand, since POST and PUT reject it with a 422. marketplace.json, the list, get, and skill hub responses treat it as unset instead of failing the request

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Link to Devin session: https://app.devin.ai/sessions/ee4e29a1f283440e865ceea49877f0fb
Open in Devin Desktop: https://app.devin.ai/desktop/session/ee4e29a1f283440e865ceea49877f0fb?variant=devin

  • 19a6c00 passes /live-pr-risk
  • 1c16857 passes /audit (rows 1 to 22 green at this tip in runs 5 and 6; row 23's gateway half live, its Claude Desktop half accepted under the audit cap as the Medium caveat)

Note

Medium Risk
Changes public marketplace.json shape and plugin admin APIs; misconfigured auto_install on non-archive sources is allowed but may confuse Claude Desktop clients.

Overview
Adds optional installation_preference (available, auto_install, required) to Claude Code marketplace plugin registration and updates. Admins set it on POST/PUT /claude-code/plugins; invalid enum values return 422, and auto_install / required without an archive source plus sha256 return 400.

When set, the value is stored in the manifest and surfaced as installationPreference on marketplace.json entries (omitted when unset). The same field is echoed on plugin list, get, /public/skill_hub, and register/update responses. Hand-edited unknown manifest values are treated as unset with a warning rather than failing the catalog.

OpenAPI snapshot and dashboard schema.d.ts are updated; coverage includes new integration and unit tests.

Reviewed by Cursor Bugbot for commit 9688a2d. Bugbot is set up for automated code reviews on this repo. Configure here.


Devin Review

…tries

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
devin-ai-integration Bot requested a review from a team September 16, 2026 15:42
@devin-ai-integration

devin-ai-integration Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@CLAassistant

CLAassistant commented Sep 16, 2026 •

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ mateo-berri
❌ devin-ai-integration[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

@codspeed

codspeed Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

⚠️ 1 benchmark spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

✅ 31 untouched benchmarks


Comparing litellm_marketplace_installation_preference (9688a2d) with main (0eff31a)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (9621a60) during the generation of this report, so 0eff31a was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@greptile-apps

greptile-apps Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Adds installation preference field to plugin marketplace.

The PR appears safe to merge based on the reviewed changes.

Summary

Adds an optional installation preference to Claude Code plugin registration and updates, emits it in marketplace entries, and exposes it through plugin read APIs. Validation, API schemas, and tests are updated. The prior worker-selection finding was manually resolved.

Reviews (7) · Last reviewed commit: "test(integration): give the marketplace ..."

@codecov

codecov Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

devin-ai-integration Bot and others added 5 commits September 16, 2026 16:10
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…values

Accept only available, auto_install, or required so a typo returns 422 instead of being silently dropped, and cover the PUT full-replace clearing behavior with a test
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

github-advanced-security[bot]

This comment was marked as resolved.

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor

cursor Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

greptile-apps[bot]

This comment was marked as resolved.

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

devin-ai-integration[bot]

This comment was marked as resolved.

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

…ccept the camelCase key, and echo the preference on POST and PUT
@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 9688a2d. Configure here.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants